/**
 * $Id: mxCodec.java,v 1.31 2010-10-18 10:45:50 gaudenz Exp $
 * Copyright (c) 2006, Gaudenz Alder
 */
package com.mxgraph.io;

import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxCellPath;
import com.mxgraph.model.mxICell;
import com.mxgraph.util.mxUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.Hashtable;
import java.util.Map;

/**
 * XML codec for Java object graphs. In order to resolve forward references
 * when reading files the XML document that contains the data must be passed
 * to the constructor.
 */
public class mxCodec {

    /**
     * Holds the owner document of the codec.
     */
    protected Document document;

    /**
     * Maps from IDs to objects.
     */
    protected Map<String, Object> objects = new Hashtable<String, Object>();

    /**
     * Specifies if default values should be encoded. Default is false.
     */
    protected boolean encodeDefaults = false;

    /**
     * Constructs an XML encoder/decoder with a new owner document.
     */
    public mxCodec() {
        this(mxUtils.createDocument());
    }

    /**
     * Constructs an XML encoder/decoder for the specified owner document.
     *
     * @param document Optional XML document that contains the data. If no document
     *                 is specified then a new document is created using mxUtils.createDocument
     */
    public mxCodec(Document document) {
        if (document == null) {
            document = mxUtils.createDocument();
        }

        this.document = document;
    }

    /**
     * Returns the owner document of the codec.
     *
     * @return Returns the owner document.
     */
    public Document getDocument() {
        return document;
    }

    /**
     * Sets the owner document of the codec.
     */
    public void setDocument(Document value) {
        document = value;
    }

    /**
     * Returns if default values of member variables should be encoded.
     */
    public boolean isEncodeDefaults() {
        return encodeDefaults;
    }

    /**
     * Sets if default values of member variables should be encoded.
     */
    public void setEncodeDefaults(boolean encodeDefaults) {
        this.encodeDefaults = encodeDefaults;
    }

    /**
     * Returns the object lookup table.
     */
    public Map<String, Object> getObjects() {
        return objects;
    }

    /**
     * Assoiates the given object with the given ID.
     *
     * @param id     ID for the object to be associated with.
     * @param object Object to be associated with the ID.
     * @return Returns the given object.
     */
    public Object putObject(String id, Object object) {
        return objects.put(id, object);
    }

    /**
     * Returns the decoded object for the element with the specified ID in
     * {@link #document}. If the object is not known then {@link #lookup(String)}
     * is used to find an object. If no object is found, then the element with
     * the respective ID from the document is parsed using {@link #decode(Node)}.
     *
     * @param id ID of the object to be returned.
     * @return Returns the object for the given ID.
     */
    public Object getObject(String id) {
        Object obj = null;

        if (id != null) {
            obj = objects.get(id);

            if (obj == null) {
                obj = lookup(id);

                if (obj == null) {
                    Node node = getElementById(id);

                    if (node != null) {
                        obj = decode(node);
                    }
                }
            }
        }

        return obj;
    }

    /**
     * Hook for subclassers to implement a custom lookup mechanism for cell IDs.
     * This implementation always returns null.
     *
     * @param id ID of the object to be returned.
     * @return Returns the object for the given ID.
     */
    public Object lookup(String id) {
        return null;
    }

    /**
     * Returns the element with the given ID from the document.
     *
     * @param id ID of the element to be returned.
     * @return Returns the element for the given ID.
     */
    public Node getElementById(String id) {
        return getElementById(id, null);
    }

    /**
     * Returns the element with the given ID from document. The optional attr
     * argument specifies the name of the ID attribute. Default is "id". The
     * XPath expression used to find the element is //*[@attr='arg'] where attr
     * is the name of the ID attribute and arg is the given id.
     * <p/>
     * Parameters:
     * <p/>
     * id - String that contains the ID.
     * attr - Optional string for the attributename. Default is id.
     */
    public Node getElementById(String id, String attr) {
        if (attr == null) {
            attr = "id";
        }

        String expr = "//*[@" + attr + "='" + id + "']";

        return mxUtils.selectSingleNode(document, expr);
    }

    /**
     * Returns the ID of the specified object. This implementation calls
     * reference first and if that returns null handles the object as an
     * mxCell by returning their IDs using mxCell.getId. If no ID exists for
     * the given cell, then an on-the-fly ID is generated using
     * mxCellPath.create.
     *
     * @param obj Object to return the ID for.
     * @return Returns the ID for the given object.
     */
    public String getId(Object obj) {
        String id = null;

        if (obj != null) {
            id = reference(obj);

            if (id == null && obj instanceof mxICell) {
                id = ((mxICell) obj).getId();

                if (id == null) {
                    // Uses an on-the-fly Id
                    id = mxCellPath.create((mxICell) obj);

                    if (id.length() == 0) {
                        id = "root";
                    }
                }
            }
        }

        return id;
    }

    /**
     * Hook for subclassers to implement a custom method for retrieving IDs from
     * objects. This implementation always returns null.
     *
     * @param obj Object whose ID should be returned.
     * @return Returns the ID for the given object.
     */
    public String reference(Object obj) {
        return null;
    }

    /**
     * Encodes the specified object and returns the resulting XML node.
     *
     * @param obj Object to be encoded.
     * @return Returns an XML node that represents the given object.
     */
    public Node encode(Object obj) {
        Node node = null;

        if (obj != null) {
            String name = mxCodecRegistry.getName(obj);
            mxObjectCodec enc = mxCodecRegistry.getCodec(name);

            if (enc != null) {
                node = enc.encode(this, obj);
            } else {
                if (obj instanceof Node) {
                    node = ((Node) obj).cloneNode(true);
                } else {
                    System.err.println("No codec for " + name);
                }
            }
        }

        return node;
    }

    /**
     * Decodes the given XML node using {@link #decode(Node,Object)}.
     *
     * @param node XML node to be decoded.
     * @return Returns an object that represents the given node.
     */
    public Object decode(Node node) {
        return decode(node, null);
    }

    /**
     * Decodes the given XML node. The optional "into" argument specifies an
     * existing object to be used. If no object is given, then a new
     * instance is created using the constructor from the codec.
     * <p/>
     * The function returns the passed in object or the new instance if no
     * object was given.
     *
     * @param node XML node to be decoded.
     * @param into Optional object to be decodec into.
     * @return Returns an object that represents the given node.
     */
    public Object decode(Node node, Object into) {
        Object obj = null;

        if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
            mxObjectCodec codec = mxCodecRegistry.getCodec(node.getNodeName());

            try {
                if (codec != null) {
                    obj = codec.decode(this, node, into);
                } else {
                    obj = node.cloneNode(true);
                    ((Element) obj).removeAttribute("as");
                }
            }
            catch (Exception e) {
                System.err.println("Cannot decode " + node.getNodeName() + ": "
                        + e.getMessage());
                e.printStackTrace();
            }
        }

        return obj;
    }

    /**
     * Encoding of cell hierarchies is built-into the core, but is a
     * higher-level function that needs to be explicitely used by the
     * respective object encoders (eg. mxModelCodec, mxChildChangeCodec
     * and mxRootChangeCodec). This implementation writes the given cell
     * and its children as a (flat) sequence into the given node. The
     * children are not encoded if the optional includeChildren is false.
     * The function is in charge of adding the result into the given node
     * and has no return value.
     *
     * @param cell            mxCell to be encoded.
     * @param node            Parent XML node to add the encoded cell into.
     * @param includeChildren Boolean indicating if the method
     *                        should include all descendents.
     */
    public void encodeCell(mxICell cell, Node node, boolean includeChildren) {
        node.appendChild(encode(cell));

        if (includeChildren) {
            int childCount = cell.getChildCount();

            for (int i = 0; i < childCount; i++) {
                encodeCell(cell.getChildAt(i), node, includeChildren);
            }
        }
    }

    /**
     * Decodes cells that have been encoded using inversion, ie. where the
     * user object is the enclosing node in the XML, and restores the group
     * and graph structure in the cells. Returns a new <mxCell> instance
     * that represents the given node.
     *
     * @param node              XML node that contains the cell data.
     * @param restoreStructures Boolean indicating whether the graph
     *                          structure should be restored by calling insert and insertEdge on the
     *                          parent and terminals, respectively.
     * @return Graph cell that represents the given node.
     */
    public mxICell decodeCell(Node node, boolean restoreStructures) {
        mxICell cell = null;

        if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
            // Tries to find a codec for the given node name. If that does
            // not return a codec then the node is the user object (an XML node
            // that contains the mxCell, aka inversion).
            mxObjectCodec decoder = mxCodecRegistry
                    .getCodec(node.getNodeName());

            // Tries to find the codec for the cell inside the user object.
            // This assumes all node names inside the user object are either
            // not registered or they correspond to a class for cells.
            if (!(decoder instanceof mxCellCodec)) {
                Node child = node.getFirstChild();

                while (child != null && !(decoder instanceof mxCellCodec)) {
                    decoder = mxCodecRegistry.getCodec(child.getNodeName());
                    child = child.getNextSibling();
                }

                String name = mxCell.class.getSimpleName();
                decoder = mxCodecRegistry.getCodec(name);
            }

            if (!(decoder instanceof mxCellCodec)) {
                String name = mxCell.class.getSimpleName();
                decoder = mxCodecRegistry.getCodec(name);
            }

            cell = (mxICell) decoder.decode(this, node);

            if (restoreStructures) {
                insertIntoGraph(cell);
            }
        }

        return cell;
    }

    /**
     * Inserts the given cell into its parent and terminal cells.
     */
    public void insertIntoGraph(mxICell cell) {
        mxICell parent = cell.getParent();
        mxICell source = cell.getTerminal(true);
        mxICell target = cell.getTerminal(false);

        // Fixes possible inconsistencies during insert into graph
        cell.setTerminal(null, false);
        cell.setTerminal(null, true);
        cell.setParent(null);

        if (parent != null) {
            parent.insert(cell);
        }

        if (source != null) {
            source.insertEdge(cell, true);
        }

        if (target != null) {
            target.insertEdge(cell, false);
        }
    }

    /**
     * Sets the attribute on the specified node to value. This is a
     * helper method that makes sure the attribute and value arguments
     * are not null.
     *
     * @param node      XML node to set the attribute for.
     * @param attribute Name of the attribute whose value should be set.
     * @param value     New value of the attribute.
     */
	public static void setAttribute(Node node, String attribute, Object value)
	{
		if (node.getNodeType() == Node.ELEMENT_NODE && attribute != null
				&& value != null)
		{
			((Element) node).setAttribute(attribute, String.valueOf(value));
		}
	}

}
